﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Diagnostics;
using System.Threading;
using RoketPack.Collections;

namespace RoketPack.Management
{
    class Installation
    {
        /// <summary>
        /// The InstallationEventArgs which allows events to recieve a references to the
        /// Installation class which issued the event.
        /// </summary>
        internal class InstallationEventArgs : EventArgs
        {
            private Installation p_Installation;

            /// <summary>
            /// Initalize the InstallationEventArgs class with a reference to the
            /// Installation instance.
            /// </summary>
            /// <param name="installation"></param>
            internal InstallationEventArgs(Installation installation)
            {
                this.p_Installation = installation;
            }

            /// <summary>
            /// The Installation instance which fired the event.
            /// </summary>
            internal Installation Installation
            {
                get
                {
                    return this.p_Installation;
                }
            }
        }

        /// <summary>
        /// An enumeration which represents the type of installation being performed.
        /// </summary>
        internal enum InstallMode
        {
            NONE,
            INSTALL,
            UPDATE,
            UNINSTALL
        }

        /// <summary>
        /// An enumeration which represents the stage that the installation is at.
        /// </summary>
        internal enum StageMode
        {
            NONE,
            DOWNLOAD,
            EXECUTION
        }

        private InstallMode p_Mode = InstallMode.NONE;
        private StageMode p_Stage = StageMode.NONE;
        private int p_Progress = 0;
        private Application p_Application = null;
        private Component p_Component = null;
        private bool p_Failed = false;
        private bool p_Complete = false;
        private IconAnimator p_Animator = null;
        private string p_TemporaryDirectory = null;
        private int p_ProcessedFiles = 0;
        private int p_TotalFiles = 0;

        // Define the InstallationProgress, InstallationComplete and InstallationFailed events.
        internal delegate void InstallationProgressHandler(object sender, InstallationEventArgs e);
        internal delegate void InstallationCompleteHandler(object sender, InstallationEventArgs e);
        internal delegate void InstallationFailedHandler(object sender, InstallationEventArgs e);

        /// <summary>
        /// Fired when the installation progresses.
        /// </summary>
        internal event InstallationProgressHandler InstallationProgress;

        /// <summary>
        /// Fired when the installation completes.
        /// </summary>
        internal event InstallationCompleteHandler InstallationComplete;

        /// <summary>
        /// Fired when the installation fails.
        /// </summary>
        internal event InstallationFailedHandler InstallationFailed;

        /// <summary>
        /// A protected function which allows for easy triggering of
        /// the InstallationProgress event from within the Installation class.
        /// </summary>
        protected virtual void OnInstallationProgress()
        {
            this.InstallationProgress(this, new InstallationEventArgs(this));
        }

        /// <summary>
        /// A protected function which allows for easy triggering of
        /// the InstallationComplete event from within the Installation class.
        /// </summary>
        protected virtual void OnInstallationComplete()
        {
            this.p_Complete = true;
            this.InstallationComplete(this, new InstallationEventArgs(this));
        }

        /// <summary>
        /// A protected function which allows for easy triggering of
        /// the InstallationFailed event from within the Installation class.
        /// </summary>
        protected virtual void OnInstallationFailed()
        {
            this.p_Failed = true;
            this.p_TotalFiles = 0;
            this.p_ProcessedFiles = 0;
            this.InstallationFailed(this, new InstallationEventArgs(this));
        }

        /// <summary>
        /// Creates a new instance of the Installation class associated with the specified component in the
        /// specified application.  Also specifies the type of installation to perform.
        /// </summary>
        /// <param name="component">The component to install.</param>
        /// <param name="application">The application which owns the above component.</param>
        /// <param name="mode">The mode of installation (install, update or uninstall).</param>
        internal Installation(Component component, Application application, InstallMode mode)
        {
            this.p_Component = component;
            this.p_Application = application;
            this.p_Mode = mode;
            this.p_Stage = StageMode.NONE;
            this.p_Progress = 0;
        }

        /// <summary>
        /// Begins an installation.
        /// </summary>
        internal void Start()
        {
            if (this.p_Stage != StageMode.NONE)
                throw new InvalidOperationException();

            // Determine the appropriate function to run in the
            // thread, based on the current mode of operation.
            ThreadStart func = null;
            if (this.p_Mode == InstallMode.INSTALL)
                func = this.HandleInstall;
            else if (this.p_Mode == InstallMode.UPDATE)
                func = this.HandleUpdate;
            else if (this.p_Mode == InstallMode.UNINSTALL)
                func = this.HandleUninstall;
            else
                throw new InvalidOperationException();

            // Create the temporary directory.
            this.p_TemporaryDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
            while (Directory.Exists(this.p_TemporaryDirectory))
                this.p_TemporaryDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
            Directory.CreateDirectory(this.p_TemporaryDirectory);

            // Start the thread.
            Thread thread = new Thread(func);
            thread.Name = "Installation Thread - " + this.p_Application.ID + ", " + this.p_Component.ID;
            thread.IsBackground = true;
            thread.Start();
        }

        /// <summary>
        /// This function handles an installation sequence.  It should be
        /// run from within a thread.
        /// </summary>
        private void HandleInstall()
        {
            try
            {
                this.p_Stage = StageMode.DOWNLOAD;
                this.p_Progress = 0;

                // First check to see if we should run in classic mode.
                if (this.p_Component.ClassicInstaller != null)
                {
                    this.p_TotalFiles = 1;
                    this.p_ProcessedFiles = 1;
                    this.OnInstallationProgress();

                    if (this.p_Component.ClassicInstaller.DownloadRequired(this))
                    {
                        // Download the installation program.
                        bool ret =
                            this.p_Component.ClassicInstaller.Download(this, (progress) =>
                            {
                                this.p_Progress = (int)progress;
                                this.OnInstallationProgress();
                            });

                        // Check to see if the download failed.
                        if (!ret)
                        {
                            this.OnInstallationFailed();
                            return;
                        }
                    }

                    this.p_Progress = 0;
                    this.p_Stage = StageMode.EXECUTION;

                    // Now install the application.
                    if (!this.p_Component.ClassicInstaller.Install(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }

                    // Finish the classic installation.
                    this.FinalizeOperation();
                    return;
                }

                // Download all of the files.
                this.p_TotalFiles = this.p_Component.Files.Count;
                this.p_ProcessedFiles = 0;

                foreach (Objects.File f in this.p_Component.Files)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (f.DownloadRequired(this))
                    {
                        bool ret =
                            f.Download(this, (progress) =>
                            {
                                this.p_Progress = (int)progress;
                                this.OnInstallationProgress();
                            });

                        // Check to see if the download failed.
                        if (!ret)
                        {
                            this.OnInstallationFailed();
                            return;
                        }
                    }
                }

                this.p_Progress = 0;
                this.p_Stage = StageMode.EXECUTION;
                this.OnInstallationProgress();

                // Now install all of the files and folders.
                this.p_TotalFiles = this.p_Component.Folders.Count + this.p_Component.Files.Count + this.p_Component.Links.Count;
                this.p_ProcessedFiles = 0;

                foreach (Objects.Folder f in this.p_Component.Folders)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Install(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                }
                foreach (Objects.File f in this.p_Component.Files)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Install(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                }
                foreach (Objects.Link f in this.p_Component.Links)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Install(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                }

                // The application is now installed.
                this.FinalizeOperation();
                return;
            }
            catch (Exception ex)
            {
                Program.HandleUnhandledException(ex);
                System.Windows.Forms.Application.Exit();
            }
        }

        /// <summary>
        /// This function handles an update sequence.  It should be run from
        /// within a thread.
        /// </summary>
        private void HandleUpdate()
        {
            try
            {
                this.p_Stage = StageMode.DOWNLOAD;
                this.p_Progress = 0;

                // First check to see if we should run in classic mode.
                if (this.p_Component.ClassicInstaller != null)
                {
                    this.p_TotalFiles = 1;
                    this.p_ProcessedFiles = 1;
                    this.OnInstallationProgress();

                    if (this.p_Component.ClassicInstaller.DownloadRequired(this))
                    {
                        // Download the installation program.
                        bool ret =
                            this.p_Component.ClassicInstaller.Download(this, (progress) =>
                            {
                                this.p_Progress = (int)progress;
                                this.OnInstallationProgress();
                            });

                        // Check to see if the download failed.
                        if (!ret)
                        {
                            this.OnInstallationFailed();
                            return;
                        }
                    }

                    this.p_Progress = 0;
                    this.p_Stage = StageMode.EXECUTION;
                    this.OnInstallationProgress();

                    // Now install the application.
                    if (!this.p_Component.ClassicInstaller.Update(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }

                    // Finish the classic installation.
                    this.FinalizeOperation();
                    return;
                }

                // Download all of the files.
                this.p_TotalFiles = this.p_Component.Files.Count;
                this.p_ProcessedFiles = 0;

                foreach (Objects.File f in this.p_Component.Files)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (f.DownloadRequired(this))
                    {
                        bool ret =
                            f.Download(this, (progress) =>
                            {
                                this.p_Progress = (int)progress;
                                this.OnInstallationProgress();
                            });

                        // Check to see if the download failed.
                        if (!ret)
                        {
                            this.OnInstallationFailed();
                            return;
                        }
                    }
                }

                this.p_Progress = 0;
                this.p_Stage = StageMode.EXECUTION;
                this.OnInstallationProgress();

                // Now update the required files and folders.
                this.p_TotalFiles = this.p_Component.Folders.Count + this.p_Component.Files.Count + this.p_Component.Links.Count;
                this.p_ProcessedFiles = 0;

                List<Objects.File> filestodelete = new List<Objects.File>();
                List<Objects.Folder> folderstodelete = new List<Objects.Folder>();
                List<Objects.Link> linkstodelete = new List<Objects.Link>();
                foreach (Objects.Folder f in this.p_Component.Folders)
                {
                    if (f.DeleteOnNextUpdate) continue;

                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Update(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                    else
                        this.p_Component.ForceInstalledVersion(f, f.AvailableVersion);
                }
                foreach (Objects.File f in this.p_Component.Files)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    bool temp = false;
                    if (this.p_Application.ID == Manager.RoketPackID && this.p_Application.Repository.Url == Manager.OfficialRepository)
                        temp = true;

                    if (!f.Update(this, temp))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                    else if (f.DeleteOnNextUpdate)
                        filestodelete.Add(f);
                    else
                        this.p_Component.ForceInstalledVersion(f, f.AvailableVersion);
                }
                foreach (Objects.Link f in this.p_Component.Links)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Update(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                    else if (f.DeleteOnNextUpdate)
                        linkstodelete.Add(f);
                    else
                        this.p_Component.ForceInstalledVersion(f, f.AvailableVersion);
                }
                foreach (Objects.Folder f in this.p_Component.Folders)
                {
                    if (!f.DeleteOnNextUpdate) continue;

                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Update(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                    else
                        folderstodelete.Add(f);
                }

                // Remove deleted files from the XmlTree and the 
                // dictionaries.
                #region XML and Dictionary Cleanup
                foreach (Objects.File f in filestodelete)
                {
                    this.p_Component.Files.Remove(f);

                    try
                    {
                        foreach (Node n in this.p_Application.XmlTree.GetChildElement("application").GetChildElements("component"))
                            if (n.GetChildElement("id").GetText().Value == this.p_Component.ID)
                            {
                                Node td = null;
                                foreach (Node fn in n.GetChildElement("installation").GetChildElements("file"))
                                    if (fn.GetChildElement("local").GetText().Value == f.Local)
                                    {
                                        td = fn;
                                        break;
                                    }
                                if (td != null)
                                    n.GetChildElement("installation").Children.Remove(td);
                            }
                    }
                    catch (KeyNotFoundException)
                    {
                        // TODO: What to do here?  There's apparently no
                        // <installation> element in the XML file to delete
                        // the nodes from.
                    }
                }
                foreach (Objects.Folder f in folderstodelete)
                {
                    this.p_Component.Folders.Remove(f);

                    try
                    {
                        foreach (Node n in this.p_Application.XmlTree.GetChildElement("application").GetChildElements("component"))
                            if (n.GetChildElement("id").GetText().Value == this.p_Component.ID)
                            {
                                Node td = null;
                                foreach (Node fn in n.GetChildElement("installation").GetChildElements("folder"))
                                    if (fn.GetChildElement("local").GetText().Value == f.Local)
                                    {
                                        td = fn;
                                        break;
                                    }
                                if (td != null)
                                    n.GetChildElement("installation").Children.Remove(td);
                            }
                    }
                    catch (KeyNotFoundException)
                    {
                        // TODO: What to do here?  There's apparently no
                        // <installation> element in the XML file to delete
                        // the nodes from.
                    }
                }
                foreach (Objects.Link f in linkstodelete)
                {
                    this.p_Component.Links.Remove(f);

                    try
                    {
                        foreach (Node n in this.p_Application.XmlTree.GetChildElement("application").GetChildElements("component"))
                            if (n.GetChildElement("id").GetText().Value == this.p_Component.ID)
                            {
                                Node td = null;
                                foreach (Node fn in n.GetChildElement("installation").GetChildElements("link"))
                                {
                                    // Normalize the link.
                                    string linkpath = fn.GetChildElement("local").GetText().Value;
                                    if (!linkpath.ToLowerInvariant().EndsWith(".lnk")) linkpath = linkpath + ".lnk";

                                    if (linkpath == f.Local)
                                    {
                                        td = fn;
                                        break;
                                    }
                                }
                                if (td != null)
                                    n.GetChildElement("installation").Children.Remove(td);
                            }
                    }
                    catch (KeyNotFoundException)
                    {
                        // TODO: What to do here?  There's apparently no
                        // <installation> element in the XML file to delete
                        // the nodes from.
                    }
                }
                #endregion

                // The application is now updated.
                this.FinalizeOperation();
                return;
            }
            catch (Exception ex)
            {
                Program.HandleUnhandledException(ex);
                System.Windows.Forms.Application.Exit();
            }
        }

        /// <summary>
        /// This function handles an uninstall sequence.  It should be run from
        /// within a thread.
        /// </summary>
        private void HandleUninstall()
        {
            try
            {
                this.p_Stage = StageMode.EXECUTION;
                this.p_Progress = 0;

                // First check to see if we should run in classic mode.
                if (this.p_Component.ClassicInstaller != null)
                {
                    this.p_TotalFiles = 1;
                    this.p_ProcessedFiles = 1;
                    this.OnInstallationProgress();

                    // Now uninstall the application.
                    if (!this.p_Component.ClassicInstaller.Uninstall(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }

                    // Finish the classic uninstallation.
                    this.FinalizeOperation();
                    return;
                }

                // Now uninstall all of the files and folders.
                this.p_TotalFiles = this.p_Component.Folders.Count + this.p_Component.Files.Count + this.p_Component.Links.Count;
                this.p_ProcessedFiles = 0;

                foreach (Objects.Link f in this.p_Component.Links)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Uninstall(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                }
                foreach (Objects.File f in this.p_Component.Files)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Uninstall(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                }
                foreach (Objects.Folder f in this.p_Component.Folders)
                {
                    this.p_ProcessedFiles += 1;
                    this.OnInstallationProgress();

                    if (!f.Uninstall(this))
                    {
                        this.OnInstallationFailed();
                        return;
                    }
                }

                // The application is now uninstalled.
                this.FinalizeOperation();
                return;
            }
            catch (Exception ex)
            {
                Program.HandleUnhandledException(ex);
                System.Windows.Forms.Application.Exit();
            }
        }

        /// <summary>
        /// Called when an installation / update / uninstallation operation
        /// finishes and the cache and other components must be updated.
        /// </summary>
        private void FinalizeOperation()
        {
            // Sometimes we have to wait until installation
            // is *really* complete (i.e. there's a delay in between
            // the installer deleting / adding files and them actually
            // begin deleted from / added to the disk).
            if (this.p_Mode == InstallMode.UNINSTALL)
            {
                double i = 3; // 3 second wait.
                while (this.p_Component.IsInstalled() && i > 0)
                {
                    Thread.Sleep(100);
                    i -= 0.1;
                }
            }
            else
            {
                double i = 3; // 3 second wait.
                while (!this.p_Component.IsInstalled() && i > 0)
                {
                    Thread.Sleep(100);
                    i -= 0.1;
                }
            }

            // Reset the TotalFiles and ProcessedFiles properties.
            this.p_TotalFiles = 0;
            this.p_ProcessedFiles = 0;

            // We no longer call ForceInstalledVersion on the component as
            // a whole, rather we call it during the update when we update
            // individual files and folders.

            // Sync the application data with the cache.
            this.p_Application.SyncCache();

            // Installation complete.
            this.p_Progress = 100;
            this.OnInstallationProgress();

            // Reset the installation state back.
            this.p_Stage = StageMode.NONE;

            this.OnInstallationComplete();
        }

        /// <summary>
        /// Whether or not the installation failed.
        /// </summary>
        internal bool Failed
        {
            get
            {
                return this.p_Failed;
            }
        }

        /// <summary>
        /// Whether or not the installation completed successfully.
        /// </summary>
        internal bool Complete
        {
            get
            {
                return this.p_Complete;
            }
        }

        /// <summary>
        /// The mode of the installation.
        /// </summary>
        internal InstallMode Mode
        {
            get
            {
                return this.p_Mode;
            }
        }

        /// <summary>
        /// The stage of the installation.
        /// </summary>
        internal StageMode Stage
        {
            get
            {
                return this.p_Stage;
            }
        }

        /// <summary>
        /// The progress of the download or installation.
        /// </summary>
        internal int Progress
        {
            get
            {
                return this.p_Progress;
            }
        }

        /// <summary>
        /// The component that is being installed.
        /// </summary>
        internal Component Component
        {
            get
            {
                return this.p_Component;
            }
        }

        /// <summary>
        /// The application that owns the component being installed.
        /// </summary>
        internal Application Application
        {
            get
            {
                return this.p_Application;
            }
        }

        /// <summary>
        /// The icon animator that's currently being used while this installation
        /// takes place.  Must be explicitly created and set elsewhere (this class
        /// does not initalize one).
        /// </summary>
        internal IconAnimator Animator
        {
            get
            {
                return this.p_Animator;
            }
            set
            {
                this.p_Animator = value;
            }
        }

        /// <summary>
        /// A temporary directory for installation data.  This directory and it's
        /// contents will be forcably removed at the end of the installation.
        /// </summary>
        internal string TemporaryDirectory
        {
            get
            {
                return this.p_TemporaryDirectory;
            }
        }

        /// <summary>
        /// The total number of files to be processed in the current stage.
        /// </summary>
        internal int TotalFiles
        {
            get
            {
                return this.p_TotalFiles;
            }
        }

        /// <summary>
        /// The current number of files that have been processed in this stage.
        /// </summary>
        internal int ProcessedFiles
        {
            get
            {
                return this.p_ProcessedFiles;
            }
        }
    }
}
